package com.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;

import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.UUID;

/**
 * @author chai
 * @since 2020/11/3
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 验证
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                // 按顺序拦截，拦截到就不继续执行
                // 权限验证
//                .antMatchers("/admin/**").hasRole("admin")
//                .antMatchers("/user/**").hasRole("user")
                // 登录方式验证
//                .antMatchers("/admin/**").fullyAuthenticated()
//                .antMatchers("/user/**").rememberMe()
                .antMatchers("/hello","/set/**", "/get/**").permitAll()
//                .antMatchers("/details").authenticated()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login.html")
                // 此部分配置可以放入自定义Filter中
//                .loginProcessingUrl("/login")
//                .usernameParameter("username")
//                .passwordParameter("password")
                // 设置为自定义的用户信息配置
//                .authenticationDetailsSource(customWebAuthenticationDetailsSource)
                .permitAll()
                .and()
            // remember-me
//            .rememberMe()
//                .rememberMeServices(rememberMeServices())
//                .rememberMeParameter("remember-me")
                // 加入数据库支持
//                .tokenRepository(jdbcTokenRepository())
//                .and()
            .logout()
//                .logoutUrl("/logout")
//                .deleteCookies()
                // 指定登出处理器
                .addLogoutHandler(rememberMeServices())
                // 登出成功处理器
                .logoutSuccessHandler((request, response, authentication) -> {
                    response.setCharacterEncoding("UTF-8");
                    response.setContentType(MediaType.TEXT_HTML_VALUE);
                    PrintWriter out = response.getWriter();
//                    User principal = (User) authentication.getPrincipal();
                    out.write("退出登录成功!");
                    out.flush();
                    out.close();
                })
            .and().csrf()
                // 禁用 csrf 防御
//                .disable();
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

        // 设置最大会话数为1 即后登录的用户踢掉之前登录的用户
        // 使用此配置 用户类需要重写equals和hasCode方法 否则无效 参考·org.springframework.security.core.userdetails.User·
        // 使用自定义Filter时 此配置无效
//        http.sessionManagement()
////                .sessionAuthenticationStrategy(sessionAuthenticationStrategy());
//                // 可以防御会话固定攻击 默认即此 不用设置
////                .sessionFixation().migrateSession()
//                .maximumSessions(1)
//        // 添加此配置后 用户已登录后就不能再次登录了
//        // 此配置需要与·HttpSessionEventPublisher·配合使用 否则退出登录后session不能正确清除 导致不能再次登录
////                .maxSessionsPreventsLogin(true)
//                .sessionRegistry(sessionRegistry());

        // 处理session并发问题 关键在于 sessionRegistry()
        http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> {
            HttpServletResponse resp = event.getResponse();
            resp.setContentType("application/json;charset=utf-8");
            resp.setStatus(401);
            PrintWriter out = resp.getWriter();
            out.write(new ObjectMapper().writeValueAsString("您已在另一台设备登录，本次登录已下线!"));
            out.flush();
            out.close();
        }), ConcurrentSessionFilter.class);
        // 与默认session配置冲突 使用此配置后 一切关于session的需重新编写
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 请求过滤
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**", "/favicon.ico");
    }

    /**
     * 授权
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
        // 该配置会覆盖掉配置文件中的配置
//        auth.inMemoryAuthentication()
//                .withUser("user")
//                .password("123456")
//                .roles("admin");
    }

    /**
     * 自定义登录过滤器
     */
    @Bean
    LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter(authenticationManagerBean());
        // 默认就是/login
//        loginFilter.setFilterProcessesUrl("/login");
//        loginFilter.setUsernameParameter("username");
//        loginFilter.setPasswordParameter("password");
        loginFilter.setAuthenticationDetailsSource(customWebAuthenticationDetailsSource);
//        loginFilter.setRememberMeServices(rememberMeServices());

        // 添加Session并发控制处理
        loginFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());

        loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            response.setCharacterEncoding("UTF-8");
            response.setContentType(MediaType.TEXT_HTML_VALUE);
            PrintWriter out = response.getWriter();
            out.write("登录成功!");
            out.flush();
            out.close();
        });
        loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
            response.setCharacterEncoding("UTF-8");
            response.setContentType(MediaType.TEXT_HTML_VALUE);
            PrintWriter out = response.getWriter();
            out.write("登录失败:" + exception.getMessage());
            out.flush();
            out.close();
        });
        return loginFilter;
    }

    @Autowired
    private DataSource dataSource;
    /** 自定义登录时保存的用户详细信息 */
    @Autowired
    private CustomWebAuthenticationDetailsSource customWebAuthenticationDetailsSource;

    /**
     * 自定义session配置管理 与自定义Filter配合使用
     */
    @Autowired
    FindByIndexNameSessionRepository sessionRepository;
    @Bean
    SessionRegistry sessionRegistry() {
        return new SpringSessionBackedSessionRegistry(sessionRepository);
//        return new SessionRegistryImpl();
    }

    /**
     * 配合·HttpSecurity#sessionManagement·使用 否则不会清除退出用户的sessionId 导致不能继续登录
     * 此配置与·Spring-Session·冲突!!!
     */
//    @Bean
//    HttpSessionEventPublisher httpSessionEventPublisher() {
//        return new HttpSessionEventPublisher();
//    }

    /**
     * 自定义Session并发控制处理策略
     */
    @Bean
    SessionAuthenticationStrategy sessionAuthenticationStrategy() {
//        CompositeSessionAuthenticationStrategy sessionAuthenticationStrategy = new CompositeSessionAuthenticationStrategy(sessionRegistry());
        ConcurrentSessionControlAuthenticationStrategy sessionAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
        // 默认即是1
        sessionAuthenticationStrategy.setMaximumSessions(1);
        // 该配置与·HttpSecurity#setMaximumSessions#maxSessionsPreventsLogin·配置作用一致
//        sessionAuthenticationStrategy.setExceptionIfMaximumExceeded(true);
        return sessionAuthenticationStrategy;
    }

    /**
     * 自定义Filter使用的RememberMe服务
     * 使用·Spring-Session·后不需要该配置了
     */
    @Bean
    AbstractRememberMeServices rememberMeServices() {
        return new CustomRememberMeServices(UUID.randomUUID().toString(),
                userDetailsService(), jdbcTokenRepository());
    }

    @Override
    @Bean
    protected AuthenticationManager authenticationManager() {
        // 加入自定义的登录处理器
        return new ProviderManager(Collections.singletonList(customAuthenticationProvider()));
    }

    /**
     * 自定义登录处理器
     */
    @Bean
    CustomAuthenticationProvider customAuthenticationProvider() {
        CustomAuthenticationProvider customAuthenticationProvider = new CustomAuthenticationProvider();
        customAuthenticationProvider.setUserDetailsService(userDetailsService());
        customAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return customAuthenticationProvider;
    }

    /**
     * 配置角色继承关系 上级角色继承下级角色所有权限
     */
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        // 需要给角色手动加上 ROLE_ 前缀
        hierarchy.setHierarchy("ROLE_admin > ROLE_user");
        return hierarchy;
    }

    /**
     * 保存token的数据源
     */
    @Bean
    JdbcTokenRepositoryImpl jdbcTokenRepository() {
        // 持久化 token
        JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(dataSource);
        return repository;
    }

    /**
     * 密码加密
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置用户信息来源
     */
    @Override
    protected UserDetailsService userDetailsService() {
        // 基于内存的用户信息
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withUsername("java-boy").password(passwordEncoder().encode("123")).roles("admin").build());
//        manager.createUser(User.withUsername("java-girl").password(passwordEncoder().encode("123")).roles("user").build());
        // 使用数据库数据
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
        manager.setDataSource(dataSource);
        if (!manager.userExists("java-boy")) {
            manager.createUser(User.withUsername("java-boy").password(passwordEncoder().encode("123")).roles("admin").build());
        }
        if (!manager.userExists("java-girl")) {
            manager.createUser(User.withUsername("java-girl").password(passwordEncoder().encode("123")).roles("user").build());
        }
        return manager;
    }

}
